/* * Copyright (C) 2017 Oasis Feng. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.oasisfeng.condom; import android.app.Application; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.IContentProvider; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.support.annotation.CheckResult; import android.support.annotation.Keep; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.Size; import android.util.Log; import com.oasisfeng.condom.util.Lazy; import java.util.List; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; /** * The condom-style {@link ContextWrapper} to prevent unwanted behaviors going through. * * Created by Oasis on 2017/3/25. */ @Keep public class CondomContext extends ContextWrapper { public static @CheckResult CondomContext wrap(final Context base, final @Nullable @Size(max=13) String tag) { return wrap(base, tag, new CondomOptions()); } /** * This is the very first (probably only) API you need to wrap the naked {@link Context} under protection of <code>CondomContext</code> * * @param base the original context used before <code>CondomContext</code> is introduced. * @param tag the optional tag to distinguish between multiple instances of <code>CondomContext</code> used parallel. */ public static @CheckResult CondomContext wrap(final Context base, final @Nullable @Size(max=13) String tag, final CondomOptions options) { if (base instanceof CondomContext) return (CondomContext) base; final Context app_context = base.getApplicationContext(); final CondomCore condom = new CondomCore(base, options); if (app_context instanceof Application) { // The application context is indeed an Application, this should be preserved semantically. final Application app = (Application) app_context; final CondomApplication condom_app = new CondomApplication(condom, app, tag); final CondomContext condom_context = new CondomContext(condom, condom_app, tag); condom_app.attachBaseContext(base == app_context ? condom_context : new CondomContext(condom, app, tag)); return condom_context; } else return new CondomContext(condom, base == app_context ? null : new CondomContext(condom, app_context, tag), tag); } /** @deprecated Use {@link CondomOptions} instead */ public CondomContext setDryRun(final boolean dry_run) { if (dry_run == mCondom.mDryRun) return this; mCondom.mDryRun = dry_run; if (dry_run) Log.w(TAG, "Start dry-run mode, no outbound requests will be blocked actually, despite later stated in log."); else Log.w(TAG, "Stop dry-run mode."); return this; } /** @deprecated Use {@link CondomOptions} instead */ @Deprecated public CondomContext preventWakingUpStoppedPackages(final boolean prevent_or_not) { mCondom.mExcludeStoppedPackages = prevent_or_not; return this; } /** @deprecated Use {@link CondomOptions} instead */ @Deprecated public CondomContext preventBroadcastToBackgroundPackages(final boolean prevent_or_not) { mCondom.mExcludeBackgroundReceivers = prevent_or_not; return this; } /** @deprecated Use {@link CondomOptions} instead */ @Deprecated public CondomContext preventServiceInBackgroundPackages(final boolean prevent_or_not) { mCondom.mExcludeBackgroundServices = prevent_or_not; return this; } /* ****** Hooked Context APIs ****** */ @Override public boolean bindService(final Intent intent, final ServiceConnection conn, final int flags) { final boolean result = mCondom.proceed(OutboundType.BIND_SERVICE, intent, Boolean.FALSE, new CondomCore.WrappedValueProcedure<Boolean>() { @Override public Boolean proceed() { return CondomContext.super.bindService(intent, conn, flags); }}); if (result) mCondom.logIfOutboundPass(TAG, intent, CondomCore.getTargetPackage(intent), CondomCore.CondomEvent.BIND_PASS); return result; } @Override public ComponentName startService(final Intent intent) { final ComponentName component = mCondom.proceed(OutboundType.START_SERVICE, intent, null, new CondomCore.WrappedValueProcedure<ComponentName>() { @Override public ComponentName proceed() { return CondomContext.super.startService(intent); }}); if (component != null) mCondom.logIfOutboundPass(TAG, intent, component.getPackageName(), CondomCore.CondomEvent.START_PASS); return component; } @Override public void sendBroadcast(final Intent intent) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendBroadcast(intent); }}); } @Override public void sendBroadcast(final Intent intent, final String receiverPermission) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendBroadcast(intent, receiverPermission); }}); } @RequiresApi(JELLY_BEAN_MR1) @Override public void sendBroadcastAsUser(final Intent intent, final UserHandle user) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendBroadcastAsUser(intent, user); }}); } @RequiresApi(JELLY_BEAN_MR1) @Override public void sendBroadcastAsUser(final Intent intent, final UserHandle user, final String receiverPermission) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendBroadcastAsUser(intent, user, receiverPermission); }}); } @Override public void sendOrderedBroadcast(final Intent intent, final String receiverPermission) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendOrderedBroadcast(intent, receiverPermission); }}); } @Override public void sendOrderedBroadcast(final Intent intent, final String receiverPermission, final BroadcastReceiver resultReceiver, final Handler scheduler, final int initialCode, final String initialData, final Bundle initialExtras) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendOrderedBroadcast(intent, receiverPermission, resultReceiver, scheduler, initialCode, initialData, initialExtras); }}); } @RequiresApi(JELLY_BEAN_MR1) @Override public void sendOrderedBroadcastAsUser(final Intent intent, final UserHandle user, final String receiverPermission, final BroadcastReceiver resultReceiver, final Handler scheduler, final int initialCode, final String initialData, final Bundle initialExtras) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendOrderedBroadcastAsUser(intent, user, receiverPermission, resultReceiver, scheduler, initialCode, initialData, initialExtras); }}); } @Override public void sendStickyBroadcast(final Intent intent) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendStickyBroadcast(intent); }}); } @RequiresApi(JELLY_BEAN_MR1) @Override public void sendStickyBroadcastAsUser(final Intent intent, final UserHandle user) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendStickyBroadcastAsUser(intent, user); }}); } @Override public void sendStickyOrderedBroadcast(final Intent intent, final BroadcastReceiver resultReceiver, final Handler scheduler, final int initialCode, final String initialData, final Bundle initialExtras) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendStickyOrderedBroadcast(intent, resultReceiver, scheduler, initialCode, initialData, initialExtras); }}); } @RequiresApi(JELLY_BEAN_MR1) @Override public void sendStickyOrderedBroadcastAsUser(final Intent intent, final UserHandle user, final BroadcastReceiver resultReceiver, final Handler scheduler, final int initialCode, final String initialData, final Bundle initialExtras) { mCondom.proceedBroadcast(intent, new CondomCore.WrappedProcedure() { @Override public void run() { CondomContext.super.sendStickyOrderedBroadcastAsUser(intent, user, resultReceiver, scheduler, initialCode, initialData, initialExtras); }}); } @Override public ContentResolver getContentResolver() { return mContentResolver.get(); } @Override public PackageManager getPackageManager() { return mPackageManager.get(); } @Override public Context getApplicationContext() { return mApplicationContext; } @Override public Context getBaseContext() { mCondom.logConcern(TAG, "getBaseContext"); return mBaseContext.get(); } /* ********************************* */ private CondomContext(final CondomCore condom, final @Nullable Context app_context, final @Nullable @Size(max=16) String tag) { super(condom.mBase); final Context base = condom.mBase; mCondom = condom; mApplicationContext = app_context != null ? app_context : this; mBaseContext = new Lazy<Context>() { @Override protected Context create() { return new PseudoContextImpl(CondomContext.this); }}; mPackageManager = new Lazy<PackageManager>() { @Override protected PackageManager create() { return new CondomPackageManager(base.getPackageManager()); }}; mContentResolver = new Lazy<ContentResolver>() { @Override protected ContentResolver create() { return new CondomContentResolver(base, base.getContentResolver()); }}; TAG = CondomCore.buildLogTag("Condom", "Condom.", tag); } CondomCore mCondom; private final Context mApplicationContext; private final Lazy<Context> mBaseContext; private final Lazy<PackageManager> mPackageManager; private final Lazy<ContentResolver> mContentResolver; final String TAG; /* ****** Internal branch functionality ****** */ class CondomPackageManager extends PackageManagerWrapper { @Override public List<ResolveInfo> queryBroadcastReceivers(final Intent intent, final int flags) { return mCondom.proceedQuery(OutboundType.QUERY_RECEIVERS, intent, new CondomCore.WrappedValueProcedure<List<ResolveInfo>>() { @Override public List<ResolveInfo> proceed() { return CondomPackageManager.super.queryBroadcastReceivers(intent, flags); }}); } @Override public List<ResolveInfo> queryIntentServices(final Intent intent, final int flags) { final int original_intent_flags = intent.getFlags(); return mCondom.proceedQuery(OutboundType.QUERY_SERVICES, intent, new CondomCore.WrappedValueProcedure<List<ResolveInfo>>() { @Override public List<ResolveInfo> proceed() { final List<ResolveInfo> result = CondomPackageManager.super.queryIntentServices(intent, flags); mCondom.filterCandidates(OutboundType.QUERY_SERVICES, intent.setFlags(original_intent_flags), result, TAG, true); return result; }}); } @Override public ResolveInfo resolveService(final Intent intent, final int flags) { final int original_intent_flags = intent.getFlags(); // Intent flags could only filter background receivers, we have to deal with services by ourselves. return mCondom.proceed(OutboundType.QUERY_SERVICES, intent, null, new CondomCore.WrappedValueProcedure<ResolveInfo>() { @Override public ResolveInfo proceed() { if (! mCondom.mExcludeBackgroundServices && mCondom.mOutboundJudge == null) return CondomPackageManager.super.resolveService(intent, flags); // Shortcut for pass-through final List<ResolveInfo> candidates = CondomPackageManager.super.queryIntentServices(intent, flags); final Intent original_intent = intent.setFlags(original_intent_flags); // Restore the intent flags early before getFirstMatch(). return mCondom.filterCandidates(OutboundType.QUERY_SERVICES, original_intent, candidates, TAG, false); }}); } @Override public ProviderInfo resolveContentProvider(final String name, final int flags) { final ProviderInfo provider = super.resolveContentProvider(name, flags); if (! mCondom.shouldAllowProvider(provider)) return null; return provider; } @Override public List<PackageInfo> getInstalledPackages(final int flags) { mCondom.logConcern(TAG, "PackageManager.getInstalledPackages"); return super.getInstalledPackages(flags); } @Override public List<ApplicationInfo> getInstalledApplications(final int flags) { mCondom.logConcern(TAG, "PackageManager.getInstalledApplications"); return super.getInstalledApplications(flags); } CondomPackageManager(final PackageManager base) { super(base); } } private class CondomContentResolver extends ContentResolverWrapper { @Override public IContentProvider acquireUnstableProvider(final Context context, final String name) { if (! mCondom.shouldAllowProvider(context, name, PackageManager.GET_UNINSTALLED_PACKAGES)) return null; return super.acquireUnstableProvider(context, name); } @Override public IContentProvider acquireProvider(final Context context, final String name) { if (! mCondom.shouldAllowProvider(context, name, PackageManager.GET_UNINSTALLED_PACKAGES)) return null; return super.acquireProvider(context, name); } CondomContentResolver(final Context context, final ContentResolver base) { super(context, base); } } private static class CondomApplication extends Application { @Override public void registerComponentCallbacks(final ComponentCallbacks callback) { if (SDK_INT >= ICE_CREAM_SANDWICH) mApplication.registerComponentCallbacks(callback); } @Override public void unregisterComponentCallbacks(final ComponentCallbacks callback) { if (SDK_INT >= ICE_CREAM_SANDWICH) mApplication.unregisterComponentCallbacks(callback); } @Override public void registerActivityLifecycleCallbacks(final ActivityLifecycleCallbacks callback) { if (SDK_INT >= ICE_CREAM_SANDWICH) mApplication.registerActivityLifecycleCallbacks(callback); } @Override public void unregisterActivityLifecycleCallbacks(final ActivityLifecycleCallbacks callback) { if (SDK_INT >= ICE_CREAM_SANDWICH) mApplication.unregisterActivityLifecycleCallbacks(callback); } @Override public void registerOnProvideAssistDataListener(final OnProvideAssistDataListener callback) { if (SDK_INT >= JELLY_BEAN_MR2) mApplication.registerOnProvideAssistDataListener(callback); } @Override public void unregisterOnProvideAssistDataListener(final OnProvideAssistDataListener callback) { if (SDK_INT >= JELLY_BEAN_MR2) mApplication.unregisterOnProvideAssistDataListener(callback); } // The actual context returned may not be semantically consistent. We'll keep an eye for it in the wild. @Override public Context getBaseContext() { mCondom.logConcern(TAG, "Application.getBaseContext"); return super.getBaseContext(); } @Override public void attachBaseContext(final Context base) { super.attachBaseContext(base); } CondomApplication(final CondomCore condom, final Application app, final @Nullable @Size(max=13) String tag) { mCondom = condom; mApplication = app; TAG = CondomCore.buildLogTag("CondomApp", "CondomApp.", tag); } private final CondomCore mCondom; private final Application mApplication; private final String TAG; } // This should act as what ContextImpl stands for in the naked Context structure. private static class PseudoContextImpl extends PseudoContextWrapper { public PseudoContextImpl(final CondomContext condom) { super(condom); } } }